/**
 * 
 */
package br.com.framework.persistencia.impl;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;

import org.apache.log4j.Logger;

import br.com.framework.persistencia.api.BaseDao;
import br.com.framework.persistencia.model.AuditedEntity;
import br.com.framework.persistencia.model.AuditedEntity.RemovidoLogicamente;
import br.com.framework.persistencia.model.BaseEntity;
import br.com.framework.persistencia.qualifier.DataRepository;
import br.com.framework.persistencia.sort.Order;

/**
 * Implementação abstrata da interface no padrão DAO (Data Access Object) que permite manipular os dados de uma entidade.
 * 
 * @author Cleber Moura
 *
 * @param <ID> representa o tipo do identificador da entidade.
 * @param <E> representa o tipo da entidade.
 */
// Indica que o gerenciamento das trasações será realizado pelo conteiner
@TransactionManagement(TransactionManagementType.CONTAINER)
// Indica que os métodos dessa classe utilizam a transação existente ou cria uma nova caso não exista.
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public abstract class BaseDaoImpl<E extends BaseEntity> implements BaseDao<E> {
	
	protected static Logger LOGGER;

	private Class<E> entityClass;

	@Inject
	@DataRepository
	private EntityManager entityManager;
	
	/**
	 * Construtor que recebe a entidade como parâmetro.
	 * @param clazz
	 */
	public BaseDaoImpl(Class<E> clazz) {
		this.entityClass = clazz;
		LOGGER = Logger.getLogger(getClass());
	}
	
	/**
	 * Retorna a classe da entidade.
	 * @return
	 */
	protected Class<E> getEntityClass() {
		return entityClass;
	}
	
	/**
	 * @return
	 */
	protected String getEntityClassName() {
		return entityClass.getSimpleName();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public E getByID(Long id) throws PersistenceException {
		return entityManager.find(entityClass, id);
	}
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll() throws PersistenceException {
		return findAll(true);
	}
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll(boolean onlyActive) throws PersistenceException {
		return findAll(-1, -1, onlyActive);
	}
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll(int firstResult, int maxResults) throws PersistenceException {
	    return findAll(firstResult, maxResults, true);
	}
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll(int firstResult, int maxResults, boolean onlyActive) throws PersistenceException {
	    return findAll(firstResult, maxResults, null, null, onlyActive);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll(int firstResult, int maxResults,
			String sortField, Order sortOrder, boolean onlyActive)
			throws PersistenceException {
		return findAll(firstResult, maxResults, sortField, sortOrder, null, onlyActive);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll(String sortField, Order sortOrder,
			boolean onlyActive) throws PersistenceException {
		return findAll(-1, -1, sortField, sortOrder, null, onlyActive);
	}
	
	
	/**
	 * Retorna o {@link Field} correspondente ao fieldName informado
	 * 
	 * @param fieldName
	 * @return
	 */
	private Field getField(String fieldName, Class<?> clazz) {
		Field field = null;
		while (BaseEntity.class.isAssignableFrom(clazz)) {
			field = null;
			try {
				field = clazz.getDeclaredField(fieldName);
			} catch (Exception e) {
				// nada a fazer
			}
			if (field != null) {
				break;
			}
			clazz = clazz.getSuperclass();
		}
		return field;
	}
	
	
	/**
	 * Constroi a query de busca de todos os registros.
	 * 
	 * @param qryStr
	 * @param filters
	 * @param sortField
	 * @param sortOrder
	 * @param onlyActive
	 * @return
	 */
	private Query createQueryBuscarTodos(boolean isQueryCount, Map<String, String> filters, String sortField, Order sortOrder, boolean onlyActive) {
		StringBuffer qryStr = new StringBuffer();
		if (isQueryCount) {
			qryStr.append("SELECT COUNT(e) FROM ");
		} else {
			qryStr.append("SELECT e FROM ");
		}
		qryStr.append(getEntityClassName());
		qryStr.append(" AS e WHERE (1=1) ");
		if (onlyActive && AuditedEntity.class.isAssignableFrom(getEntityClass())) {
	    	qryStr.append("AND e.removido = :removido ");
	    }
		
		if (filters != null && !filters.isEmpty()) {
			for (Entry<String, String> entry : filters.entrySet()) {
				String fieldName = entry.getKey();
				Field field = getField(fieldName, getEntityClass());
				if (field != null) {
					if (String.class.isAssignableFrom(field.getType())) {
						qryStr.append("AND LOWER(e." + fieldName + ") LIKE LOWER(:"+ fieldName +") ");
					} else {
						qryStr.append("AND e." + fieldName + " = "+ entry.getValue() +" ");
					}
				} else {
					throw new PersistenceException("O atributo "+entry.getKey()+" não pode ser utilizado para o filtro.");
				}
			}
		}
		if (sortField != null && !sortField.trim().isEmpty()) {
			qryStr.append("ORDER BY e.");
			qryStr.append(sortField + " " + sortOrder.getOrder());
		}
	    Query query = entityManager.createQuery(qryStr.toString());
	    if (onlyActive && AuditedEntity.class.isAssignableFrom(getEntityClass())) {
	    	query.setParameter("removido", RemovidoLogicamente.NAO);
	    }
	    if (filters != null && !filters.isEmpty()) {
			for (Entry<String, String> entry : filters.entrySet()) {
				String fieldName = entry.getKey();
				Field field = getField(fieldName, getEntityClass());
				if (String.class.isAssignableFrom(field.getType())) {
					query.setParameter(entry.getKey(), "%" + entry.getValue() + "%");
				}
			}
	    }
	    return query;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<E> findAll(int firstResult, int maxResults,
			String sortField, Order sortOrder, Map<String, String> filters,
			boolean onlyActive) throws PersistenceException {
		Query query = createQueryBuscarTodos(false, filters, sortField, sortOrder, onlyActive);
	    if (firstResult > -1 && maxResults > -1){
	    	query.setFirstResult(firstResult);
	    	query.setMaxResults(maxResults);
		}
		return (List<E>) query.getResultList();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Long getRecordCount() throws PersistenceException {
		return getRecordCount(true);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Long getRecordCount(boolean onlyActive) throws PersistenceException {
		return getRecordCount(null, onlyActive);
	}
	
	@Override
	public Long getRecordCount(Map<String, String> filters,
			boolean onlyActive) throws PersistenceException {
		Query query = createQueryBuscarTodos(true, filters, null, null, onlyActive);
		return (Long) query.getSingleResult();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void insert(E entidade) throws PersistenceException {
		entityManager.persist(entidade);
	}
	/**
	 * {@inheritDoc}
	 */
	public void update(E entidade) throws PersistenceException {
		entityManager.merge(entidade);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void refresh(E entidade) throws PersistenceException {
		entityManager.refresh(entidade);
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(E entidade) throws PersistenceException {
		remove(entidade, true);
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(E entidade, boolean removerLogicamente) throws PersistenceException {
		if ((entidade instanceof AuditedEntity) && removerLogicamente) {
			((AuditedEntity)entidade).setRemovido(RemovidoLogicamente.SIM);
			this.update(entidade);
		} else {
			entityManager.remove(entidade);
		}
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(Long id) throws PersistenceException {
		remove(id, true);
		
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(Long id, boolean isLogicalDelete) throws PersistenceException {
		E entidade = getByID(id);
		if (entidade != null) {
			remove(entidade, isLogicalDelete);
		} else {
			throw new EntityNotFoundException("Entidade/ID: " + getEntityClass().getSimpleName() + "/" + id); 
		}
	}
	/**
	 * {@inheritDoc}
	 */
	public List<E> buscarPorCriteria(CriteriaQuery<E> criteriaQuery)
			throws PersistenceException {
		TypedQuery<E> tQuery = entityManager.createQuery(criteriaQuery);
		return tQuery.getResultList();
	}

	/**
	 * @return
	 */
	protected EntityManager getEntityManager() {
		return entityManager;
	}
	
	/**
	 * @param entityManager the entityManager to set
	 */
	protected void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}
	
}
